/* Copyright 2024 Marimo. All rights reserved. */

import { act, render } from "@testing-library/react";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import type { z } from "zod";
import { initialModeAtom } from "@/core/mode";
import { store } from "@/core/state/jotai";
import type { IPluginProps } from "../../types";
import { NumberPlugin } from "../NumberPlugin";

describe("NumberPlugin", () => {
  beforeEach(() => {
    vi.useFakeTimers();
    store.set(initialModeAtom, "edit");
  });

  afterEach(() => {
    vi.useRealTimers();
  });

  it("renders with initial value and updates correctly", () => {
    const plugin = new NumberPlugin();
    const host = document.createElement("div");
    const setValue = vi.fn();

    // Initial render with value 5
    const props: IPluginProps<
      number | null,
      z.infer<(typeof plugin)["validator"]>
    > = {
      host,
      value: 5,
      setValue,
      data: {
        start: 0,
        stop: 10,
        step: 1,
        label: null,
        debounce: false,
        fullWidth: false,
      },
      functions: {},
    };

    const { getByRole, rerender } = render(plugin.render(props));

    // Wait for React-Aria NumberField to initialize
    act(() => {
      vi.advanceTimersByTime(0);
    });

    const input = getByRole("textbox", {
      name: "Number input",
    }) as HTMLInputElement;
    expect(input).toBeTruthy();
    expect(input.value).toBe("5");

    // Update to value 7
    const updatedProps = { ...props, value: 7 };
    rerender(plugin.render(updatedProps));
    expect(input.value).toBe("7");

    // Reset to value 0
    const resetProps = { ...props, value: 0 };
    rerender(plugin.render(resetProps));
    expect(input.value).toBe("0");
  });

  it("handles null values correctly", () => {
    const plugin = new NumberPlugin();
    const host = document.createElement("div");
    const setValue = vi.fn();

    const props: IPluginProps<
      number | null,
      z.infer<(typeof plugin)["validator"]>
    > = {
      host,
      value: null,
      setValue,
      data: {
        start: 0,
        stop: 10,
        step: 1,
        label: null,
        debounce: false,
        fullWidth: false,
      },
      functions: {},
    };

    const { getByRole } = render(plugin.render(props));

    act(() => {
      vi.advanceTimersByTime(0);
    });

    const input = getByRole("textbox", {
      name: "Number input",
    }) as HTMLInputElement;

    // Null values should render as empty (NaN is used internally)
    expect(input.value).toBe("");
  });

  it("handles NaN values correctly", () => {
    const plugin = new NumberPlugin();
    const host = document.createElement("div");
    const setValue = vi.fn();

    const props: IPluginProps<
      number | null,
      z.infer<(typeof plugin)["validator"]>
    > = {
      host,
      value: Number.NaN,
      setValue,
      data: {
        start: 0,
        stop: 10,
        step: 1,
        label: null,
        debounce: false,
        fullWidth: false,
      },
      functions: {},
    };

    const { getByRole } = render(plugin.render(props));

    act(() => {
      vi.advanceTimersByTime(0);
    });

    const input = getByRole("textbox", {
      name: "Number input",
    }) as HTMLInputElement;

    // NaN values should be filtered to null and render as empty
    expect(input.value).toBe("");
  });

  it("handles debounce correctly", () => {
    const plugin = new NumberPlugin();
    const host = document.createElement("div");
    const setValue = vi.fn();

    const props: IPluginProps<
      number | null,
      z.infer<(typeof plugin)["validator"]>
    > = {
      host,
      value: 5,
      setValue,
      data: {
        start: 0,
        stop: 10,
        step: 1,
        label: null,
        debounce: true,
        fullWidth: false,
      },
      functions: {},
    };

    const { getByRole, rerender } = render(plugin.render(props));

    act(() => {
      vi.advanceTimersByTime(0);
    });

    // setValue should not be called yet
    expect(setValue).not.toHaveBeenCalled();

    // Update the value prop to simulate a change
    const updatedProps = { ...props, value: 8 };
    rerender(plugin.render(updatedProps));

    // setValue should not be called immediately due to debounce
    expect(setValue).not.toHaveBeenCalled();

    // Advance timers by debounce delay (200ms)
    act(() => {
      vi.advanceTimersByTime(200);
    });

    // Now verify the component can handle debounced state
    const input = getByRole("textbox", {
      name: "Number input",
    }) as HTMLInputElement;
    expect(input.value).toBe("8");
  });

  it("handles debounce disabled (immediate updates)", () => {
    const plugin = new NumberPlugin();
    const host = document.createElement("div");
    const setValue = vi.fn();

    const props: IPluginProps<
      number | null,
      z.infer<(typeof plugin)["validator"]>
    > = {
      host,
      value: 5,
      setValue,
      data: {
        start: 0,
        stop: 10,
        step: 1,
        label: null,
        debounce: false,
        fullWidth: false,
      },
      functions: {},
    };

    const { getByRole, rerender } = render(plugin.render(props));

    act(() => {
      vi.advanceTimersByTime(0);
    });

    // setValue should not be called yet
    expect(setValue).not.toHaveBeenCalled();

    // Update the value prop to simulate a change
    const updatedProps = { ...props, value: 8 };
    rerender(plugin.render(updatedProps));

    // With debounce disabled, the value should update immediately without delay
    const input = getByRole("textbox", {
      name: "Number input",
    }) as HTMLInputElement;
    expect(input.value).toBe("8");

    // No need to advance timers since debounce is disabled
    expect(setValue).not.toHaveBeenCalled(); // setValue is only called when user interacts, not on prop changes
  });

  it("respects min and max values", () => {
    const plugin = new NumberPlugin();
    const host = document.createElement("div");
    const setValue = vi.fn();

    const props: IPluginProps<
      number | null,
      z.infer<(typeof plugin)["validator"]>
    > = {
      host,
      value: 5,
      setValue,
      data: {
        start: 0,
        stop: 10,
        step: 1,
        label: null,
        debounce: false,
        fullWidth: false,
      },
      functions: {},
    };

    const { getByRole, rerender } = render(plugin.render(props));

    act(() => {
      vi.advanceTimersByTime(0);
    });

    const input = getByRole("textbox", {
      name: "Number input",
    }) as HTMLInputElement;

    expect(input.value).toBe("5");

    // Test that values above max are clamped to max
    const aboveMaxProps = { ...props, value: 15 };
    rerender(plugin.render(aboveMaxProps));
    expect(input.value).toBe("10"); // NumberField clamps to max value

    // Test that values below min are clamped to min
    const belowMinProps = { ...props, value: -5 };
    rerender(plugin.render(belowMinProps));
    expect(input.value).toBe("0"); // NumberField clamps to min value
  });

  it("handles disabled state", () => {
    const plugin = new NumberPlugin();
    const host = document.createElement("div");
    const setValue = vi.fn();

    const props: IPluginProps<
      number | null,
      z.infer<(typeof plugin)["validator"]>
    > = {
      host,
      value: 5,
      setValue,
      data: {
        start: 0,
        stop: 10,
        step: 1,
        label: null,
        debounce: false,
        fullWidth: false,
        disabled: true,
      },
      functions: {},
    };

    const { getByRole } = render(plugin.render(props));

    act(() => {
      vi.advanceTimersByTime(0);
    });

    const input = getByRole("textbox", {
      name: "Number input",
    }) as HTMLInputElement;

    expect(input.disabled).toBe(true);
  });

  it("handles step values", () => {
    const plugin = new NumberPlugin();
    const host = document.createElement("div");
    const setValue = vi.fn();

    const props: IPluginProps<
      number | null,
      z.infer<(typeof plugin)["validator"]>
    > = {
      host,
      value: 5,
      setValue,
      data: {
        start: 0,
        stop: 10,
        step: 0.5,
        label: null,
        debounce: false,
        fullWidth: false,
      },
      functions: {},
    };

    const { getByRole } = render(plugin.render(props));

    act(() => {
      vi.advanceTimersByTime(0);
    });

    const input = getByRole("textbox", {
      name: "Number input",
    }) as HTMLInputElement;

    expect(input).toBeTruthy();
    expect(input.value).toBe("5");
  });

  it("renders with custom label", () => {
    const plugin = new NumberPlugin();
    const host = document.createElement("div");
    const setValue = vi.fn();

    const props: IPluginProps<
      number | null,
      z.infer<(typeof plugin)["validator"]>
    > = {
      host,
      value: 5,
      setValue,
      data: {
        start: 0,
        stop: 10,
        step: 1,
        label: "Custom Label",
        debounce: false,
        fullWidth: false,
      },
      functions: {},
    };

    const { getByRole } = render(plugin.render(props));

    act(() => {
      vi.advanceTimersByTime(0);
    });

    const input = getByRole("textbox", {
      name: "Custom Label",
    }) as HTMLInputElement;

    expect(input).toBeTruthy();
  });

  it("handles transitions from null to number and back", () => {
    const plugin = new NumberPlugin();
    const host = document.createElement("div");
    const setValue = vi.fn();

    const props: IPluginProps<
      number | null,
      z.infer<(typeof plugin)["validator"]>
    > = {
      host,
      value: null,
      setValue,
      data: {
        start: 0,
        stop: 10,
        step: 1,
        label: null,
        debounce: false,
        fullWidth: false,
      },
      functions: {},
    };

    const { getByRole, rerender } = render(plugin.render(props));

    act(() => {
      vi.advanceTimersByTime(0);
    });

    const input = getByRole("textbox", {
      name: "Number input",
    }) as HTMLInputElement;

    expect(input.value).toBe("");

    // Update to a number
    const updatedProps = { ...props, value: 5 };
    rerender(plugin.render(updatedProps));
    expect(input.value).toBe("5");

    // Back to null
    const nullProps = { ...props, value: null };
    rerender(plugin.render(nullProps));
    expect(input.value).toBe("");
  });

  it("handles fullWidth prop", () => {
    const plugin = new NumberPlugin();
    const host = document.createElement("div");
    const setValue = vi.fn();

    const props: IPluginProps<
      number | null,
      z.infer<(typeof plugin)["validator"]>
    > = {
      host,
      value: 5,
      setValue,
      data: {
        start: 0,
        stop: 10,
        step: 1,
        label: null,
        debounce: false,
        fullWidth: true,
      },
      functions: {},
    };

    const { container } = render(plugin.render(props));

    act(() => {
      vi.advanceTimersByTime(0);
    });

    // Check that the NumberField has the full width class
    const numberField = container.querySelector(
      '[data-testid="marimo-plugin-number-input"]',
    );
    expect(numberField?.classList.contains("w-full")).toBe(true);
  });
});
